/*
 *  $Id$
 *  Copyright [2010] [Panxiaobo] 
 *  Licensed under the Apache License, Version 2.0 (the "License"); 
 *  you may not use this file except in compliance with the License. 
 *  You may obtain a copy of the License at
 *  
 *     http://www.apache.org/licenses/LICENSE-2.0
 *     
 *  Unless required by applicable law or agreed to in writing, software 
 *  distributed under the License is distributed on an "AS IS" BASIS, 
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 *  See the License for the specific language governing permissions and 
 *  limitations under the License. 
 */
package pxb.openid.rp;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import pxb.openid.OpenIdException;
import pxb.openid.util.IO;
import pxb.util.http.HttpConnector;
import pxb.util.http.HttpConnector.HttpResponse;
import pxb.util.http.impl.SimpleHttpConnectorImpl;

/**
 * 根据openid查找对应的OP
 * 
 * @author Panxiaobo
 * 
 */
public class Discoverer {
    public static final class OpenIdService implements Comparable<OpenIdService> {
        private String openidDelegate;
        private int priority = 9999;
        private String type;
        private String uri;

        @Override
        public int compareTo(OpenIdService o) {
            return this.priority - o.priority;
        }

        /**
         * @return the openidDelegate
         */
        public String getOpenidDelegate() {
            return openidDelegate;
        }

        /**
         * @return the priority
         */
        public int getPriority() {
            return priority;
        }

        public String getType() {
            return type;
        }

        public String getUri() {
            return uri;
        }

        /**
         * @param openidDelegate
         *            the openidDelegate to set
         */
        public void setOpenidDelegate(String openidDelegate) {
            this.openidDelegate = openidDelegate;
        }

        /**
         * @param priority
         *            the priority to set
         */
        public void setPriority(int priority) {
            this.priority = priority;
        }

        public void setType(String type) {
            this.type = type;
        }

        public void setUri(String uri) {
            this.uri = uri;
        }
    }

    private static final Logger log = LoggerFactory.getLogger(Discoverer.class);

    private HttpConnector httpConnector;

    /**
	 * 
	 */
    public Discoverer() {
        this(new SimpleHttpConnectorImpl());
    }

    /**
     * @param httpConnector
     */
    public Discoverer(HttpConnector httpConnector) {
        this.httpConnector = httpConnector;
    }

    public String discover(String openId) {

        log.debug("start decover server of openid :{}", openId);
        List<OpenIdService> services = new ArrayList<OpenIdService>();

        if (openId == null) {
            throw new OpenIdException("openid is null");
        }
        try {
            if (!(openId.startsWith("http://") || openId.startsWith("https://"))) {
                openId = "http://" + openId;
            }
            doDiscover(openId, services);
        } catch (Exception e) {
            throw new OpenIdException("Error discover OpenId Provider", e);
        }
        if (services.isEmpty()) {
            throw new OpenIdException("Can't find any Provider for openid:" + openId);
        }

        for (OpenIdService service : services) {
            if ("http://specs.openid.net/auth/2.0/server".equals(service.getType())
                    || "http://specs.openid.net/auth/2.0/signon".equals(service.getType())) {
                return service.getUri();
            }
        }
        return services.get(0).getUri();
    }

    private void doDiscover(String identityUrl, List<OpenIdService> services) throws IOException,
            ParserConfigurationException, SAXException {
        BufferedReader in = null;

        HttpResponse get = null;
        try {
            log.debug("Send requet: {}", identityUrl);
            Map<String, String> headers = new HashMap<String, String>();
            headers.put("Accept", "text/html,application/xhtml+xml,application/xml,application/xrds+xml");
            headers.put("Accept-Charset", "ISO-8859-1,utf-8");
            Map<String, String> params = new HashMap<String, String>();
            params.put("name", "openid_identifier");
            get = httpConnector.get(identityUrl, headers, params);

            String contentType = get.getHead("Content-Type");
            log.debug("Response contentType: {}", contentType);
            if (contentType != null && contentType.contains("application/xrds+xml")) {
                log.debug("contentType contains 'application/xrds+xml'");
                findInXrds(get, services);
                return;
            }

            String locationHeader = get.getHead("X-XRDS-Location");
            if (locationHeader != null) {
                // then we go to this URL
                get.close();
                log.debug("Found 'X-XRDS-Location' header: {}", locationHeader);
                log.debug("Send requet: {}", locationHeader);
                HttpResponse get2 = null;
                try {
                    get2 = httpConnector.get(locationHeader);
                    findInXrds(get2, services);
                } finally {
                    if (get2 != null) {
                        get2.close();
                    }
                }
                return;
            } else {
                in = new BufferedReader(new InputStreamReader(get.getInputStream()));
                findLinkTag(services, IO.toString(in));
                return;
            }
        } finally {
            if (in != null)
                in.close();
            if (get != null)
                get.close();
        }
    }

    private void findInXrds(HttpResponse get, List<OpenIdService> services) throws ParserConfigurationException,
            SAXException, IOException {

        DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
        javax.xml.parsers.DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
        Document document = docBuilder.parse(new BufferedInputStream(get.getInputStream()));
        get.close();
        NodeList list = document.getElementsByTagName("Service");
        for (int i = 0; i < list.getLength(); i++) {
            Element node = (Element) list.item(i);
            OpenIdService service = new OpenIdService();
            try {
                service.setPriority(Integer.parseInt(node.getAttribute("priority")));
            } catch (Exception e) {
            }

            // get Types
            NodeList list2 = node.getElementsByTagName("Type");
            if (list2.getLength() < 1) {
                throw new OpenIdException("No Type Elements in Service");
            }
            if (list2.getLength() > 1) {
                log.warn("mutiple Type Elements in Service, use the first");
                if (log.isDebugEnabled()) {
                    for (int j = 0; j < list.getLength(); j++) {
                        log.debug("type:{}", list2.item(j).getTextContent());
                    }
                }
            }
            service.setType(list2.item(0).getTextContent());
            // get URI
            list2 = node.getElementsByTagName("URI");
            service.setUri(list2.item(0).getTextContent());

            list2 = node.getChildNodes();
            for (int j = 0; j < list2.getLength(); j++) {
                Node node2 = list2.item(j);
                String name = node2.getNodeName();
                if (name.endsWith(":Delegate")) {
                    service.setOpenidDelegate(node2.getTextContent());
                    break;
                }
            }
            services.add(service);
        }
    }

    public void findLinkTag(List<OpenIdService> services, String buf) {
        Map<String, String> pas = new HashMap<String, String>();
        int pos = 0;
        do {
            int start = buf.indexOf("<link ", pos);
            if (start < 0)
                break;
            pos = buf.indexOf('>', start);
            String line;
            int end;
            if (pos > 0) {
                end = pos;
            } else {
                end = buf.length();
            }
            if (buf.charAt(end - 1) == '/') {
                end--;
            }

            line = buf.substring(start + 6, end);
            String ss[] = line.split(" ");
            Map<String, String> maps = new HashMap<String, String>();
            for (String s : ss) {
                String xx[] = s.split("=");
                if (xx.length > 1) {
                    maps.put(xx[0], xx[1].replaceAll("\"", ""));
                }
            }
            String res = maps.get("rel");
            String href = maps.get("href");
            if (res != null && href != null) {
                pas.put(res, href);
            }

        } while (pos > 0);

        String server = pas.get("openid.server");
        String delegate = pas.get("openid.delegate");
        if (server != null || delegate != null) {
            OpenIdService service = new OpenIdService();
            service.setUri(server);
            service.setOpenidDelegate(delegate);
            services.add(service);
        }
    }

    /**
     * @param httpConnector
     *            the httpConnector to set
     */
    public void setHttpConnector(HttpConnector httpConnector) {
        this.httpConnector = httpConnector;
    }

}
